/*
 * Copyright (C) 2005 Luca Veltri - University of Parma - Italy
 * 
 * This file is part of MjSip (http://www.mjsip.org)
 * 
 * MjSip is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * MjSip is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with MjSip; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Author(s):
 * Luca Veltri (luca.veltri@unipr.it)
 */

package org.zoolu.sip.provider;


import org.zoolu.net.*;
import org.zoolu.sip.header.*;
import org.zoolu.sip.message.Message;
import org.zoolu.sip.address.*;
import org.zoolu.sip.transaction.Transaction;
import org.zoolu.tools.Configure;
import org.zoolu.tools.Configurable;
import org.zoolu.tools.Parser;
import org.zoolu.tools.Random;
import org.zoolu.tools.Log;
import org.zoolu.tools.LogLevel;
import org.zoolu.tools.RotatingLog;
//import org.zoolu.tools.MD5;
import org.zoolu.tools.SimpleDigest;
import org.zoolu.tools.DateFormat;

import java.util.Hashtable;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
//PersonalJava
//import java.util.HashSet;
//import java.util.Iterator;
import org.zoolu.tools.HashSet;
import org.zoolu.tools.Iterator;
import java.util.Enumeration;
import java.util.Vector;
import java.util.Date;


/** SipProvider implements the SIP transport layer, that is the layer responsable for
  * sending and receiving SIP messages. Messages are received by the callback function
  * defined in the interface SipProviderListener.
  * <p>
  * SipProvider implements also multiplexing/demultiplexing service through the use of
  * SIP interface identifiers and <i>onReceivedMessage()<i/> callback function
  * of specific SipProviderListener.
  * <p>
  * A SipProviderListener can be added to a SipProvider through the
  * addSipProviderListener(id,listener) method, where:
  * <b> - <i>id<i/> is the SIP interface identifier the listener has to be bound to,
  * <b> - <i>listener<i/> is the SipProviderListener that received messages are passed to.
  * <p/>
  * The SIP interface identifier specifies the type of messages the listener is going to
  * receive for. Together with the specific SipProvider, it represents the complete SIP
  * Service Access Point (SAP) address/identifier used for demultiplexing SIP messages
  * at receiving side. 
  * <p/>
  * The identifier can be of one of the three following types: transaction_id, dialog_id,
  * or method_id. These types of identifiers characterize respectively:
  * <br> - messages within a specific transaction,
  * <br> - messages within a specific dialog,
  * <br> - messages related to a specific SIP method.
  * It is also possible to use the the identifier ANY to specify 
  * <br> - all messages that are out of any transactions, dialogs, or already specified
  *        method types.
  * <p>
  * When receiving a message, the SipProvider first tries to look for a matching  
  * transaction, then looks for a matching dialog, then for a matching method type,
  * and finally for a default listener (i.e. that with identifier ANY).
  * For the matched SipProviderListener, the method  <i>onReceivedMessage()</i> is fired.
  * <p>
  * Note: no 482 (Loop Detected) responses are generated for requests that does not
  * properly match any ongoing transactions, dialogs, nor method types.
  */
public class SipProvider implements Configurable, TransportListener, TcpServerListener
{

   // **************************** Constants ****************************

   /** UDP protocol type */
   public static final String PROTO_UDP="udp";
   /** TCP protocol type */
   public static final String PROTO_TCP="tcp";
   /** TLS protocol type */
   public static final String PROTO_TLS="tls";
   /** SCTP protocol type */
   public static final String PROTO_SCTP="sctp";
  
   /** String value "auto-configuration" used for auto configuration of the host address. */
   public static final String AUTO_CONFIGURATION="AUTO-CONFIGURATION";

   /** String value "auto-configuration" used for auto configuration of the host address. */
   public static final String ALL_INTERFACES="ALL-INTERFACES";

   /** String value "NO-OUTBOUND" used for setting no outbound proxy. */
   //public static final String NO_OUTBOUND="NO-OUTBOUND";

   /** Identifier used as listener id for capturing ANY incoming messages
     * that does not match any active method_id, transaction_id, nor dialog_id.
     * <br> In this context, "active" means that there is a active listener
     * for that specific method, transaction, or dialog. */
   public static final Identifier ANY=new Identifier("ANY"); 

   /** Identifier used as listener id for capturing any incoming messages in PROMISQUE mode,
     * that means that messages are passed to the present listener regardless of
     * any other active SipProviderListeners for specific messages.
     * <p/>
     * More than one SipProviderListener can be added and be active concurrently
     * for capturing messages in PROMISQUE mode. */
   public static final Identifier PROMISQUE=new Identifier("PROMISQUE"); 


   /** Minimum length for a valid SIP message.  */
   private static final int MIN_MESSAGE_LENGTH=12;

   // ***************** Readable/configurable attributes *****************

   /** Via address/name.
     * Use 'auto-configuration' for auto detection, or let it undefined. */
   String via_addr=null;

   /** Local SIP port */
   int host_port=0;

   /** Network interface (IP address) used by SIP.
     * Use 'ALL-INTERFACES' for binding SIP to all interfaces (or let it undefined). */
   String host_ifaddr=null;

   /** Transport protocols (the first protocol is used as default) */
   String[] transport_protocols=null;

   /** Max number of (contemporary) open connections */
   int nmax_connections=0;

   /** Outbound proxy (host_addr[:host_port]).
     * Use 'NONE' for not using an outbound proxy (or let it undefined). */
   SocketAddress outbound_proxy=null;

   /** Whether logging all packets (including non-SIP keepalive tokens). */
   boolean log_all_packets=false;


   // for backward compatibility:

   /** Outbound proxy addr (for backward compatibility). */
   private String outbound_addr=null;
   /** Outbound proxy port (for backward compatibility). */
   private int outbound_port=-1;


   // ********************* Non-readable attributes *********************

   /** Event Loger */
   protected Log event_log=null;

   /** Message Loger */
   protected Log message_log=null;

   /** Network interface (IP address) used by SIP. */
   IpAddress host_ipaddr=null;

   /** Default transport */
   String default_transport=null;

   /** Whether using UDP as transport protocol */
   boolean transport_udp=false;
   /** Whether using TCP as transport protocol */
   boolean transport_tcp=false;
   /** Whether using TLS as transport protocol */
   boolean transport_tls=false;
   /** Whether using SCTP as transport protocol */
   boolean transport_sctp=false;

   /** Whether adding 'rport' parameter on outgoing requests. */
   boolean rport=true;
   
   /** Whether forcing 'rport' parameter on incoming requests ('force-rport' mode). */
   boolean force_rport=false;

   /** List of provider listeners */
   Hashtable listeners=null;
   
   /** List of exception listeners */
   HashSet exception_listeners=null;
   
   /** UDP transport */
   UdpTransport udp=null;   
   
   /** Tcp server */
   TcpServer tcp_server=null;

   /** Connections */
   Hashtable connections=null;  
   

   // *************************** Costructors ***************************

   /** Creates a void SipProvider. */ 
   /*protected SipProvider()
   {  
   }*/

   /** Creates a new SipProvider. */ 
   public SipProvider(String via_addr, int port)
   {  init(via_addr,port,null,null);
      initlog();
      startTrasport();
   }


   /** Creates a new SipProvider. 
     * Costructs the SipProvider, initializing the SipProviderListeners, the transport protocols, and other attributes. */ 
   public SipProvider(String via_addr, int port, String[] protocols, String ifaddr)
   {  init(via_addr,port,protocols,ifaddr);
      initlog();
      startTrasport();
   }


   /** Creates a new SipProvider. 
     * The SipProvider attributres are read from file. */ 
   public SipProvider(String file)
   {  if (!SipStack.isInit()) SipStack.init(file);
      new Configure(this,file);
      init(via_addr,host_port,transport_protocols,host_ifaddr);
      initlog();
      startTrasport();
   }


   /** Inits the SipProvider, initializing the SipProviderListeners, the transport protocols, the outbound proxy, and other attributes. */ 
   private void init(String viaddr, int port, String[] protocols, String ifaddr)
   {  if (!SipStack.isInit()) SipStack.init();
      via_addr=viaddr;
      if (via_addr==null || via_addr.equalsIgnoreCase(AUTO_CONFIGURATION)) via_addr=IpAddress.getLocalHostAddress().toString();
      host_port=port;
      if (host_port<=0) host_port=SipStack.default_port;
      host_ipaddr=null;
      if (ifaddr!=null && !ifaddr.equalsIgnoreCase(ALL_INTERFACES))
      {  try {  host_ipaddr=IpAddress.getByName(ifaddr);  } catch (IOException e) {  e.printStackTrace(); host_ipaddr=null;  }
      }
      transport_protocols=protocols;
      if (transport_protocols==null) transport_protocols=SipStack.default_transport_protocols;
      default_transport=transport_protocols[0];
      for (int i=0; i<transport_protocols.length; i++)
      {  transport_protocols[i]=transport_protocols[i].toLowerCase();
         if (transport_protocols[i].equals(PROTO_UDP)) transport_udp=true;
         else
         if (transport_protocols[i].equals(PROTO_TCP)) transport_tcp=true;
         /*
         else
         if (transport_protocols[i].equals(PROTO_TLS)) transport_tls=true;
         else
         if (transport_protocols[i].equals(PROTO_SCTP)) transport_sctp=true;
         */
      }
      if (nmax_connections<=0) nmax_connections=SipStack.default_nmax_connections;

      // just for backward compatibility..
      if (outbound_port<0) outbound_port=SipStack.default_port;
      if (outbound_addr!=null)
      {  if (outbound_addr.equalsIgnoreCase(Configure.NONE) || outbound_addr.equalsIgnoreCase("NO-OUTBOUND")) outbound_proxy=null;
         else outbound_proxy=new SocketAddress(outbound_addr,outbound_port);
      }
      
      rport=SipStack.use_rport; 
      force_rport=SipStack.force_rport; 
      
      exception_listeners=new HashSet();
      listeners=new Hashtable();

      connections=new Hashtable();
   }

   
   /** Inits logs. */ 
   private void initlog()
   {  if (SipStack.debug_level>0)
      {  String filename=SipStack.log_path+"//"+via_addr+"."+host_port;
         event_log=new RotatingLog(filename+"_events.log",null,SipStack.debug_level,SipStack.max_logsize*1024,SipStack.log_rotations,SipStack.rotation_scale,SipStack.rotation_time);
         message_log=new RotatingLog(filename+"_messages.log",null,SipStack.debug_level,SipStack.max_logsize*1024,SipStack.log_rotations,SipStack.rotation_scale,SipStack.rotation_time);
      }
      printLog("Date: "+DateFormat.formatHHMMSS(new Date()),LogLevel.HIGH);
      printLog("SipStack: "+SipStack.release,LogLevel.HIGH);
      printLog("new SipProvider(): "+toString(),LogLevel.HIGH);
   }
 
  
   /** Starts the transport services. */ 
   private void startTrasport()
   {  
      // start udp
      if (transport_udp)
      {  try
         {  if (host_ipaddr==null) udp=new UdpTransport(host_port,this);
            else udp=new UdpTransport(host_port,host_ipaddr,this);
            printLog("udp is up",LogLevel.MEDIUM);
         }
         catch (Exception e)
         {  printException(e,LogLevel.HIGH);
         }
      }
      // start tcp
      if (transport_tcp)
      {  try
         {  if (host_ipaddr==null) tcp_server=new TcpServer(host_port,this);
            else tcp_server=new TcpServer(host_port,host_ipaddr,this);
            printLog("tcp is up",LogLevel.MEDIUM);
         }
         catch (Exception e)
         {  printException(e,LogLevel.HIGH);
         }
      }
      //printLog("transport is up",LogLevel.MEDIUM);
   }

   
   /** Stops the transport services. */ 
   private void stopTrasport()
   {  
      // stop udp
      if (udp!=null)
      {  printLog("udp is going down",LogLevel.LOWER);
         udp.halt();
         udp=null;
      }
      // stop tcp
      if (tcp_server!=null)
      {  printLog("tcp is going down",LogLevel.LOWER);
         tcp_server.halt();
         tcp_server=null;
      }
      if (connections!=null)
      {  printLog("connections are going down",LogLevel.LOWER);
         for (Enumeration e=connections.elements(); e.hasMoreElements(); )
         {  ConnectedTransport c=(ConnectedTransport)e.nextElement();
            c.halt();
         }
         connections=null;
      }
   }

   /** Stops the SipProviders. */ 
   public void halt()
   {  printLog("halt: SipProvider is going down",LogLevel.MEDIUM);
      stopTrasport();
      listeners=new Hashtable();
      exception_listeners=new HashSet();
   }


   /** Parses a single line (loaded from the config file) */
   public void parseLine(String line)
   {  String attribute;
      Parser par;
      int index=line.indexOf("=");
      if (index>0) {  attribute=line.substring(0,index).trim(); par=new Parser(line,index+1);  }
      else {  attribute=line; par=new Parser("");  }
      char[] delim={' ',','};
      
      if (attribute.equals("via_addr")) {  via_addr=par.getString(); return;  }
      if (attribute.equals("host_port")) {  host_port=par.getInt(); return; }
      if (attribute.equals("host_ifaddr")) {  host_ifaddr=par.getString(); return;  }
      if (attribute.equals("transport_protocols")) {  transport_protocols=par.getWordArray(delim); return;  }
      if (attribute.equals("nmax_connections")) {  nmax_connections=par.getInt(); return;  }
      if (attribute.equals("outbound_proxy"))
      {  String soaddr=par.getString();
         if (soaddr==null || soaddr.length()==0 || soaddr.equalsIgnoreCase(Configure.NONE) || soaddr.equalsIgnoreCase("NO-OUTBOUND")) outbound_proxy=null;
         else outbound_proxy=new SocketAddress(soaddr);
         return;
      }
      if (attribute.equals("log_all_packets")) { log_all_packets=(par.getString().toLowerCase().startsWith("y")); return; }

      // old parameters
      if (attribute.equals("host_addr")) System.err.println("WARNING: parameter 'host_addr' is no more supported; use 'via_addr' instead.");
      if (attribute.equals("all_interfaces")) System.err.println("WARNING: parameter 'all_interfaces' is no more supported; use 'host_iaddr' for setting a specific interface or let it undefined.");
      if (attribute.equals("use_outbound")) System.err.println("WARNING: parameter 'use_outbound' is no more supported; use 'outbound_proxy' for setting an outbound proxy or let it undefined.");
      if (attribute.equals("outbound_addr"))
      {  System.err.println("WARNING: parameter 'outbound_addr' has been deprecated; use 'outbound_proxy=<host_addr>[:<host_port>]' instead.");
         outbound_addr=par.getString();
         return;
      }
      if (attribute.equals("outbound_port"))
      {  System.err.println("WARNING: parameter 'outbound_port' has been deprecated; use 'outbound_proxy=<host_addr>[:<host_port>]' instead.");
         outbound_port=par.getInt();
         return;
      }
   }  


   /** Converts the entire object into lines (to be saved into the config file) */
   protected String toLines()
   {  // currently not implemented..
      return toString();
   }


   /** Gets a String with the list of transport protocols. */ 
   private String transportProtocolsToString()
   {  String list=transport_protocols[0];
      for (int i=1; i<transport_protocols.length; i++) list+="/"+transport_protocols[i];
      return list;
   }


   // ************************** Public methods *************************

   /** Gets via address. */ 
   public String getViaAddress()
   {  return via_addr;
   }    
   
   /** Sets via address. */ 
   /*public void setViaAddress(String addr)
   {  via_addr=addr;
   }*/   

   /** Gets host port. */ 
   public int getPort()
   {  return host_port;
   }       

   /** Whether binding the sip provider to all interfaces or only on the specified host address. */
   public boolean isAllInterfaces()
   {  return host_ipaddr==null;
   }       

   /** Gets host interface IpAddress. */ 
   public IpAddress getInterfaceAddress()
   {  return host_ipaddr;
   }    
   
   /** Gets array of transport protocols. */ 
   public String[] getTransportProtocols()
   {  return transport_protocols;
   }    
   
   /** Gets the default transport protocol. */ 
   public String getDefaultTransport()
   {  return default_transport;
   } 
   
   /** Gets the default transport protocol. */ 
   public void setDefaultTransport(String proto)
   {  default_transport=proto;
   }    

   /** Sets rport support. */ 
   public void setRport(boolean flag)
   {  rport=flag;
   }   

   /** Whether using rport. */ 
   public boolean isRportSet()
   {  return rport;
   }   

   /** Sets 'force-rport' mode. */ 
   public void setForceRport(boolean flag)
   {  force_rport=flag;
   }   

   /** Whether using 'force-rport' mode. */ 
   public boolean isForceRportSet()
   {  return force_rport;
   }   

   /** Whether has outbound proxy. */ 
   public boolean hasOutboundProxy()
   {  return outbound_proxy!=null;
   }    

   /** Gets the outbound proxy. */ 
   public SocketAddress getOutboundProxy()
   {  return outbound_proxy;
   }    

   /** Sets the outbound proxy. Use 'null' for not using any outbound proxy. */ 
   public void setOutboundProxy(SocketAddress soaddr)
   {  outbound_proxy=soaddr;
   }

   /** Removes the outbound proxy. */ 
   /*public void removeOutboundProxy()
   {  setOutboundProxy(null);
   }*/

   /** Gets the max number of (contemporary) open connections. */ 
   public int getNMaxConnections()
   {  return nmax_connections;
   }    

   /** Sets the max number of (contemporary) open connections. */ 
   public void setNMaxConnections(int n)
   {  nmax_connections=n;
   }    
      
            
   /** Gets event log. */ 
   public Log getLog()
   {  return event_log;
   }    
   

   /** Returns the list (Hashtable) of active listener_IDs. */ 
   public Hashtable getListeners()
   {  return listeners;
   }   


   /** Adds a new listener to the SipProvider for caputering any message in PROMISQUE mode.
     * It is the same as using method addSipProviderListener(SipProvider.PROMISQUE,listener).
     * <p/>
     * When capturing messages in promisque mode all messages are passed to the SipProviderListener
     * before passing them to the specific listener (if present).
     * <br/> Note that more that one SipProviderListener can be active in promisque mode
     * at the same time;in that case the same message is passed to all PROMISQUE
     * SipProviderListeners.
     * @param listener is the SipProviderListener.
     * @return It returns <i>true</i> if the SipProviderListener is added,
     * <i>false</i> if the listener_ID is already in use. */
   public boolean addSipProviderPromisqueListener(SipProviderListener listener)
   {  return addSipProviderListener(PROMISQUE,listener);
   }

   /** Adds a new listener to the SipProvider for caputering ANY message.
     * It is the same as using method addSipProviderListener(SipProvider.ANY,listener).
     * @param listener is the SipProviderListener.
     * @return It returns <i>true</i> if the SipProviderListener is added,
     * <i>false</i> if the listener_ID is already in use. */
   public boolean addSipProviderListener(SipProviderListener listener)
   {  return addSipProviderListener(ANY,listener);
   }

   /** Adds a new listener to the SipProvider.
     * @param id is the unique identifier for the messages which the listener
     * as to be associated to. It is used as key.
     * It can identify a method, a transaction, or a dialog.
     * Use SipProvider.ANY to capture all messages.
     * Use SipProvider.PROMISQUE if you want to capture all message in promisque mode
     * (letting other listeners to capture the same received messages).
     * @param listener is the SipProviderListener for this message id.
     * @return It returns <i>true</i> if the SipProviderListener is added,
     * <i>false</i> if the listener_ID is already in use. */
   public boolean addSipProviderListener(Identifier id, SipProviderListener listener)
   {  printLog("adding SipProviderListener: "+id,LogLevel.MEDIUM);
      boolean ret;
      Identifier key=id;
      if (listeners.containsKey(key))
      {  printWarning("trying to add a SipProviderListener with a id that is already in use.",LogLevel.HIGH);
         ret=false;
      }
      else
      {  listeners.put(key,listener);
         ret=true;
      }
      
      if (listeners!=null)
      {  String list="";
         for (Enumeration e=listeners.keys(); e.hasMoreElements();) list+=e.nextElement()+", ";
         printLog(listeners.size()+" listeners: "+list,LogLevel.LOW);
      }
      return ret;
   }


   /** Removes a SipProviderListener.
     * @param id is the unique identifier used to select the listened messages.
     * @return It returns <i>true</i> if the SipProviderListener is removed,
     * <i>false</i> if the  identifier is missed. */
   public boolean removeSipProviderListener(Identifier id)
   {  printLog("removing SipProviderListener: "+id,LogLevel.MEDIUM);
      boolean ret;
      Identifier key=id;
      if (!listeners.containsKey(key))
      {  printWarning("trying to remove a missed SipProviderListener.",LogLevel.HIGH);
         ret=false;
      }
      else
      {  listeners.remove(key);
         ret=true;
      }
      
      if (listeners!=null)
      {  String list="";
         for (Enumeration e=listeners.keys(); e.hasMoreElements();) list+=e.nextElement()+", ";
         printLog(listeners.size()+" listeners: "+list,LogLevel.LOW);
      }
      return ret;
   }

  
   /** Sets the SipProviderExceptionListener.
     * The SipProviderExceptionListener is the listener for all exceptions
     * thrown by the SipProviders.
     * @param e_listener is the SipProviderExceptionListener.
     * @return It returns <i>true</i> if the SipProviderListener has been correctly set,
     * <i>false</i> if the SipProviderListener was already set. */
   public boolean addSipProviderExceptionListener(SipProviderExceptionListener e_listener)
   {  printLog("adding SipProviderExceptionListener",LogLevel.MEDIUM);
      if (exception_listeners.contains(e_listener))
      {  printWarning("trying to add an already present SipProviderExceptionListener.",LogLevel.HIGH);
         return false;
      }
      else
      {  exception_listeners.add(e_listener);
         return true;
      }
   }


   /** Removes a SipProviderExceptionListener. 
     * @param e_listener is the SipProviderExceptionListener.
     * @return It returns <i>true</i> if the SipProviderExceptionListener has been correctly removed,
     * <i>false</i> if the SipProviderExceptionListener is missed. */
   public boolean removeSipProviderExceptionListener(SipProviderExceptionListener e_listener)
   {  printLog("removing SipProviderExceptionListener",LogLevel.MEDIUM);
      if (!exception_listeners.contains(e_listener))
      {  printWarning("trying to remove a missed SipProviderExceptionListener.",LogLevel.HIGH);
         return false;
      }
      else
      {  exception_listeners.remove(e_listener);
         return true;
      }
   }


   /** Sends a Message, specifing the transport portocol, nexthop address and port.
     * <p> This is a low level method and
     * forces the message to be routed to a specific nexthop address, port and transport,
     * regardless whatever the Via, Route, or request-uri, address to. 
     * <p>
     * In case of connection-oriented transport, the connection is selected as follows:
     * <br> - if an existing connection is found matching the destination
     *        end point (socket), such connection is used, otherwise
     * <br> - a new connection is established
     *
     * @return It returns a Connection in case of connection-oriented delivery
     * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP)
     */
   public ConnectionIdentifier sendMessage(Message msg, String proto, String dest_addr, int dest_port, int ttl)
   {  if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Resolving host address '"+dest_addr+"'",LogLevel.MEDIUM);
      try
      {  IpAddress dest_ipaddr=IpAddress.getByName(dest_addr);  
         return sendMessage(msg,proto,dest_ipaddr,dest_port,ttl); 
      }
      catch (Exception e)
      {  printException(e,LogLevel.HIGH);
         return null;
      }     
   }

   /** Sends a Message, specifing the transport portocol, nexthop address and port. */
   private ConnectionIdentifier sendMessage(Message msg, String proto, IpAddress dest_ipaddr, int dest_port, int ttl)
   {  ConnectionIdentifier conn_id=new ConnectionIdentifier(proto,dest_ipaddr,dest_port);
      if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Sending message to "+conn_id,LogLevel.MEDIUM);

      if (transport_udp && proto.equals(PROTO_UDP))
      {  // UDP
         //printLog("using UDP",LogLevel.LOW);     
         conn_id=null;
         try
         {  // if (ttl>0 && multicast_address) do something?
            udp.sendMessage(msg,dest_ipaddr,dest_port);
         }
         catch (IOException e)
         {  printException(e,LogLevel.HIGH);
            return null;
         }
      }
      else
      if (transport_tcp && proto.equals(PROTO_TCP))
      {  // TCP
         //printLog("using TCP",LogLevel.LOW);
         if (!connections.containsKey(conn_id))
         {  printLog("no active connection found matching "+conn_id,LogLevel.MEDIUM);
            printLog("open "+proto+" connection to "+dest_ipaddr+":"+dest_port,LogLevel.MEDIUM);
            TcpTransport conn=null;
            try
            {  conn=new TcpTransport(dest_ipaddr,dest_port,this);
            }
            catch (Exception e)
            {  printLog("connection setup FAILED",LogLevel.HIGH);
               return null;
            }
            printLog("connection "+conn+" opened",LogLevel.HIGH);
            addConnection(conn);
         }
         else
         {  printLog("active connection found matching "+conn_id,LogLevel.MEDIUM);
         }
         ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id);
         if (conn!=null)
         {  printLog("sending data through conn "+conn,LogLevel.MEDIUM);
            try
            {  conn.sendMessage(msg);
               conn_id=new ConnectionIdentifier(conn);
            }
            catch (IOException e)
            {  printException(e,LogLevel.HIGH);
               return null;
            }
         }
         else
         {  // this point has not to be reached
            printLog("ERROR: conn "+conn_id+" not found: abort.",LogLevel.MEDIUM);
            return null;
         }
      }
      else
      {  // otherwise
         printWarning("Unsupported protocol ("+proto+"): Message discarded",LogLevel.HIGH);
         return null;
      }
      // logs
      String dest_addr=dest_ipaddr.toString();
      printMessageLog(proto,dest_addr,dest_port,msg.getLength(),msg,"sent");
      return conn_id;
   }


   /** Sends the message <i>msg</i>.
     * <p>
     * The destination for the request is computed as follows:
     * <br> - if <i>outbound_addr</i> is set, <i>outbound_addr</i> and 
     *        <i>outbound_port</i> are used, otherwise
     * <br> - if message has Route header with lr option parameter (i.e. RFC3261 compliant),
     *        the first Route address is used, otherwise
     * <br> - the request's Request-URI is considered.
     * <p>
     * The destination for the response is computed based on the sent-by parameter in
     *     the Via header field (RFC3261 compliant)
     * <p>
     * As transport it is used the protocol specified in the 'via' header field 
     * <p>
     * In case of connection-oriented transport:
     * <br> - if an already established connection is found matching the destination
     *        end point (socket), such connection is used, otherwise
     * <br> - a new connection is established
     *
     * @return Returns a ConnectionIdentifier in case of connection-oriented delivery
     * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP)
     */
   public ConnectionIdentifier sendMessage(Message msg)
   {  printLog("Sending message:\r\n"+msg.toString(),LogLevel.LOWER);

      // select the transport protocol
      ViaHeader via=msg.getViaHeader();
      String proto=via.getProtocol().toLowerCase();
      printLog("using transport "+proto,LogLevel.MEDIUM);
      
      // select the destination address and port
      String dest_addr=null;
      int dest_port=0;
      int ttl=0;
      
      if (msg.isRequest())
      {  // REQUESTS
         if (outbound_proxy!=null)
         {  dest_addr=outbound_proxy.getAddress().toString();
            dest_port=outbound_proxy.getPort();
         }
         else 
         {  if (msg.hasRouteHeader() && msg.getRouteHeader().getNameAddress().getAddress().hasLr())
            {  
               SipURL url=msg.getRouteHeader().getNameAddress().getAddress();
               dest_addr=url.getHost();
               dest_port=url.getPort();
            }
            else
            {  SipURL url=msg.getRequestLine().getAddress();
               dest_addr=url.getHost();
               dest_port=url.getPort();
               if (url.hasMaddr())
               {  dest_addr=url.getMaddr();
                  if (url.hasTtl()) ttl=url.getTtl();
                  // update the via header by adding maddr and ttl params 
                  via.setMaddr(dest_addr);
                  if (ttl>0) via.setTtl(ttl);
                  msg.removeViaHeader();
                  msg.addViaHeader(via);
               }
            }
         }
      }
      else
      {  // RESPONSES
         SipURL url=via.getSipURL();
         if (via.hasReceived()) dest_addr=via.getReceived(); else dest_addr=url.getHost();
         if (via.hasRport()) dest_port=via.getRport();
         if (dest_port<=0) dest_port=url.getPort();
      }
      
      if (dest_port<=0) dest_port=SipStack.default_port;  
      
      return sendMessage(msg,proto,dest_addr,dest_port,ttl); 
   }


   /** Sends the message <i>msg</i> using the specified connection. */
   public ConnectionIdentifier sendMessage(Message msg, ConnectionIdentifier conn_id)
   {  if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Sending message through conn "+conn_id,LogLevel.HIGH);
      printLog("message:\r\n"+msg.toString(),LogLevel.LOWER);

      if (conn_id!=null && connections.containsKey(conn_id))
      {  // connection exists
         printLog("active connection found matching "+conn_id,LogLevel.MEDIUM);
         ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id);
         try
         {  conn.sendMessage(msg);
            // logs
            //String proto=conn.getProtocol();
            String proto=conn.getProtocol();
            String dest_addr=conn.getRemoteAddress().toString();
            int dest_port=conn.getRemotePort();
            printMessageLog(proto,dest_addr,dest_port,msg.getLength(),msg,"sent");
            return conn_id;
         }
         catch (Exception e)
         {  printException(e,LogLevel.HIGH);
         }
      }
      //else
      printLog("no active connection found matching "+conn_id,LogLevel.MEDIUM);
      return sendMessage(msg);
   }


   /** Processes the message received.
     * It is called each time a new message is received by the transport layer, and
     * it performs the actual message processing. */ 
   protected void processReceivedMessage(Message msg)
   {  try
      {  // logs
         printMessageLog(msg.getTransportProtocol(),msg.getRemoteAddress(),msg.getRemotePort(),msg.getLength(),msg,"received");

         // discard too short messages
         if (msg.getLength()<=2)
         {  if (log_all_packets) printLog("message too short: discarded\r\n",LogLevel.LOW);
            return;
         }
         // discard non-SIP messages
         String first_line=msg.getFirstLine();
         if (first_line==null || first_line.toUpperCase().indexOf("SIP/2.0")<0)
         {  if (log_all_packets) printLog("NOT a SIP message: discarded\r\n",LogLevel.LOW);
            return;
         }
         printLog("received new SIP message",LogLevel.HIGH);
         printLog("message:\r\n"+msg.toString(),LogLevel.LOWER);
         
         // if a request, handle "received" and "rport" parameters
         if (msg.isRequest())
         {  ViaHeader vh=msg.getViaHeader();
            boolean via_changed=false;
            String src_addr=msg.getRemoteAddress();
            int src_port=msg.getRemotePort();
            String via_addr=vh.getHost();
            int via_port=vh.getPort();
            if (via_port<=0) via_port=SipStack.default_port;
             
            if (!via_addr.equals(src_addr))
            {  vh.setReceived(src_addr);
               via_changed=true;
            }
            
            if (vh.hasRport())
            {  vh.setRport(src_port);
               via_changed=true;
            }
            else
            {  if (force_rport && via_port!=src_port)
               {  vh.setRport(src_port);
                  via_changed=true;
               }
            }
            
            if (via_changed)
            {  msg.removeViaHeader();
               msg.addViaHeader(vh);
            }
         }
         
         // is there any listeners?
         if (listeners==null || listeners.size()==0)
         {  printLog("no listener found: meesage discarded.",LogLevel.HIGH);
            return;
         }

         // try to look for a UA in promisque mode
         if (listeners.containsKey(PROMISQUE))
         {  printLog("message passed to uas: "+PROMISQUE,LogLevel.MEDIUM);
            ((SipProviderListener)listeners.get(PROMISQUE)).onReceivedMessage(this,msg);
         }

         // after the callback check if the message is still valid
         if (!msg.isRequest() && !msg.isResponse())
         {  printLog("No valid SIP message: message discarded.",LogLevel.HIGH);
            return;
         }

         // this was the promisque listener; now keep on looking for a tighter listener..
         
         // try to look for a transaction
         Identifier key=msg.getTransactionId();
         printLog("DEBUG: transaction-id: "+key,LogLevel.MEDIUM);
         if (listeners.containsKey(key))
         {  printLog("message passed to transaction: "+key,LogLevel.MEDIUM);
            ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg);
            return;
         }
         // try to look for a dialog
         key=msg.getDialogId();
         printLog("DEBUG: dialog-id: "+key,LogLevel.MEDIUM);
         if (listeners.containsKey(key))
         {  printLog("message passed to dialog: "+key,LogLevel.MEDIUM);
            ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg);
            return;
         }
         // try to look for a UAS
         key=msg.getMethodId();
         if (listeners.containsKey(key))
         {  printLog("message passed to uas: "+key,LogLevel.MEDIUM);
            ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg);
            return;
         }        
         // try to look for a default UA
         if (listeners.containsKey(ANY))
         {  printLog("message passed to uas: "+ANY,LogLevel.MEDIUM);
            ((SipProviderListener)listeners.get(ANY)).onReceivedMessage(this,msg);
            return;
         }
                   
         // if we are here, no listener_ID matched..
         printLog("No SipListener found matching that message: message DISCARDED",LogLevel.HIGH);
         //printLog("Pending SipProviderListeners= "+getListeners().size(),3);
         printLog("Pending SipProviderListeners= "+listeners.size(),LogLevel.MEDIUM);
      }
      catch (Exception e)
      {  printWarning("Error handling a new incoming message",LogLevel.HIGH);
         printException(e,LogLevel.MEDIUM);
         if (exception_listeners==null || exception_listeners.size()==0)
         {  System.err.println("Error handling a new incoming message");
            e.printStackTrace();
         }
         else
         {  for (Iterator i=exception_listeners.iterator(); i.hasNext(); )
               try
               {  ((SipProviderExceptionListener)i.next()).onMessageException(msg,e);
               }
               catch (Exception e2)
               {  printWarning("Error handling handling the Exception",LogLevel.HIGH);
                  printException(e2,LogLevel.MEDIUM);
               }
         }
      }
   }   


   /** Adds a new Connection */ 
   private void addConnection(ConnectedTransport conn)
   {  ConnectionIdentifier conn_id=new ConnectionIdentifier(conn);
      if (connections.containsKey(conn_id))
      {  // remove the previous connection
         printLog("trying to add the already established connection "+conn_id,LogLevel.HIGH);
         printLog("connection "+conn_id+" will be replaced",LogLevel.HIGH);
         ConnectedTransport old_conn=(ConnectedTransport)connections.get(conn_id);
         old_conn.halt();
         connections.remove(conn_id);
      }
      else
      if (connections.size()>=nmax_connections)
      {  // remove the older unused connection
         printLog("reached the maximum number of connection: removing the older unused connection",LogLevel.HIGH);
         long older_time=System.currentTimeMillis();
         ConnectionIdentifier older_id=null;
         for (Enumeration e=connections.elements(); e.hasMoreElements(); )
         {  ConnectedTransport co=(ConnectedTransport)e.nextElement();
            if (co.getLastTimeMillis()<older_time) older_id=new ConnectionIdentifier(co);
         }
         if (older_id!=null) removeConnection(older_id);
      }
      connections.put(conn_id,conn);
      conn_id=new ConnectionIdentifier(conn);
      conn=(ConnectedTransport)connections.get(conn_id);
      // DEBUG log:
      printLog("active connenctions:",LogLevel.LOW);
      for (Enumeration e=connections.keys(); e.hasMoreElements(); )
      {  ConnectionIdentifier id=(ConnectionIdentifier)e.nextElement();
         printLog("conn-id="+id+": "+((ConnectedTransport)connections.get(id)).toString(),LogLevel.LOW);
      }
   }

 
   /** Removes a Connection */ 
   private void removeConnection(ConnectionIdentifier conn_id)
   {  if (connections.containsKey(conn_id))
      {  ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id);
         conn.halt();
         connections.remove(conn_id);
         // DEBUG log:
         printLog("active connenctions:",LogLevel.LOW);
         for (Enumeration e=connections.elements(); e.hasMoreElements(); )
         {  ConnectedTransport co=(ConnectedTransport)e.nextElement();
            printLog("conn "+co.toString(),LogLevel.LOW);
         }
      }
   }


   //************************* Callback methods *************************
   
   /** When a new SIP message is received. */
   public void onReceivedMessage(Transport transport, Message msg)
   {  processReceivedMessage(msg);
   }   


   /** When Transport terminates. */
   public void onTransportTerminated(Transport transport, Exception error)
   {  printLog("transport "+transport+" terminated",LogLevel.MEDIUM);
      if (transport.getProtocol().equals(PROTO_TCP))
      {  ConnectionIdentifier conn_id=new ConnectionIdentifier((ConnectedTransport)transport);
         removeConnection(conn_id);
      }
      if (error!=null)
         printException(error,LogLevel.HIGH);
   }   


   /** When a new incoming Connection is established */ 
   public void onIncomingConnection(TcpServer tcp_server, TcpSocket socket)
   {  printLog("incoming connection from "+socket.getAddress()+":"+socket.getPort(),LogLevel.MEDIUM);
      ConnectedTransport conn=new TcpTransport(socket,this);
      printLog("tcp connection "+conn+" opened",LogLevel.MEDIUM);
      addConnection(conn);
   }


   /** When TcpServer terminates. */
   public void onServerTerminated(TcpServer tcp_server, Exception error) 
   {  printLog("tcp server "+tcp_server+" terminated",LogLevel.MEDIUM);
   }



   //************************** Other methods ***************************
   
   /** Picks a fresh branch value.
     * The branch ID MUST be unique across space and time for
     * all requests sent by the UA.
     * The branch ID always begin with the characters "z9hG4bK". These
     * 7 characters are used by RFC 3261 as a magic cookie. */
   public static String pickBranch()
   {  //String str=Long.toString(Math.abs(Random.nextLong()),16);
      //if (str.length()<5) str+="00000";
      //return "z9hG4bK"+str.substring(0,5);
      return "z9hG4bK"+Random.nextNumString(5);
   }  

   /** Picks an unique branch value based on a SIP message.
     * This value could also be used as transaction ID */
   public String pickBranch(Message msg)
   {  StringBuffer sb=new StringBuffer();
      sb.append(msg.getRequestLine().getAddress().toString());
      sb.append(getViaAddress()+getPort());
      ViaHeader top_via=msg.getViaHeader();
      if (top_via.hasBranch())
         sb.append(top_via.getBranch());
      else
      {  sb.append(top_via.getHost()+top_via.getPort());
         sb.append(msg.getCSeqHeader().getSequenceNumber());
         sb.append(msg.getCallIdHeader().getCallId());
         sb.append(msg.getFromHeader().getTag());
         sb.append(msg.getToHeader().getTag());
      }
      //return "z9hG4bK"+(new MD5(unique_str)).asHex().substring(0,9);
      return "z9hG4bK"+(new SimpleDigest(5,sb.toString())).asHex();
   }  


   /** Picks a new tag.
     * A tag  MUST be globally unique and cryptographically random
     * with at least 32 bits of randomness.  A property of this selection
     * requirement is that a UA will place a different tag into the From
     * header of an INVITE than it would place into the To header of the
     * response to the same INVITE.  This is needed in order for a UA to
     * invite itself to a session. */
   public static String pickTag()
   {  //String str=Long.toString(Math.abs(Random.nextLong()),16);
      //if (str.length()<8) str+="00000000";
      //return str.substring(0,8);
      return "z9hG4bK"+Random.nextNumString(8);
   }   

   /** Picks a new tag. The tag is generated uniquely based on message <i>req</i>.
     * This tag can be generated for responses in a stateless
     * manner - in a manner that will generate the same tag for the
     * same request consistently.
     */
   public static String pickTag(Message req)
   {  //return String.valueOf(tag_generator++);
      //return (new MD5(request.toString())).asHex().substring(0,8);
      return (new SimpleDigest(8,req.toString())).asHex();
   }


   /** Picks a new call-id.
     * The call-id is a globally unique
     * identifier over space and time. It is implemented in the
     * form "localid@host". Call-id must be considered case-sensitive and is
     * compared byte-by-byte. */
   public String pickCallId()
   {  //String str=Long.toString(Math.abs(Random.nextLong()),16);
      //if (str.length()<12) str+="000000000000";
      //return str.substring(0,12)+"@"+getViaAddress();
      return Random.nextNumString(12)+"@"+getViaAddress();
   }   


   /** picks an initial CSeq */
   public static int pickInitialCSeq()
   {  return 1;
   }   


   /** (<b>Deprecated</b>) Constructs a NameAddress based on an input string.
     * The input string can be a:
     * <br> - <i>user</i> name,
     * <br> - <i>user@address</i> url,
     * <br> - <i>"Name" &lt;sip:user@address&gt;</i> address,
     * <p>
     * In the former case,
     * a SIP URL is costructed using the outbound proxy as host address if present,
     * otherwise the local via address is used. */
   public NameAddress completeNameAddress(String str)
   {  if (str.indexOf("<sip:")>=0) return new NameAddress(str);
      else
      {  SipURL url=completeSipURL(str);
         return new NameAddress(url);
      }
   }
   /** Constructs a SipURL based on an input string. */
   private SipURL completeSipURL(String str)
   {  // in case it is passed only the 'user' field, add '@'<outbound_proxy>[':'<outbound_port>]
      if (!str.startsWith("sip:") && str.indexOf("@")<0 && str.indexOf(".")<0 && str.indexOf(":")<0)
      {  // may be it is just the user name..
         String url="sip:"+str+"@";
         if (outbound_proxy!=null)
         {  url+=outbound_proxy.getAddress().toString();
            int port=outbound_proxy.getPort();
            if (port>0 && port!=SipStack.default_port) url+=":"+port;
         }
         else
         {  url+=via_addr;
            if (host_port>0 && host_port!=SipStack.default_port) url+=":"+host_port;
         }
         return new SipURL(url);
      }
      else return new SipURL(str);
   }
   
   /** Constructs a SipURL for the given <i>username</i> on the local SIP UA.
     * If <i>username</i> is null, only host address and port are used. */
   /*public SipURL getSipURL(String user_name)
   {  return new SipURL(user_name,via_addr,(host_port!=SipStack.default_port)?host_port:-1);
   }*/


   //******************************* Logs *******************************

   /** Gets a String value for this object */ 
   public String toString()
   {  if (host_ipaddr==null) return host_port+"/"+transportProtocolsToString();
      else return host_ipaddr.toString()+":"+host_port+"/"+transportProtocolsToString();
   }   

   /** Adds a new string to the default Log */
   private final void printLog(String str, int level)
   {  if (event_log!=null)
      {  String provider_id=(host_ipaddr==null)? Integer.toString(host_port) : host_ipaddr.toString()+":"+host_port;
         event_log.println("SipProvider-"+provider_id+": "+str,level+SipStack.LOG_LEVEL_TRANSPORT);  
      }
   }
  
   /** Adds a WARNING to the default Log */
   private final void printWarning(String str, int level)
   {  printLog("WARNING: "+str,level); 
   }

   /** Adds the Exception message to the default Log */
   private final void printException(Exception e, int level)
   {  if (event_log!=null) event_log.printException(e,level+SipStack.LOG_LEVEL_TRANSPORT);
   }
  
   /** Adds the SIP message to the messageslog */
   private final void printMessageLog(String proto, String addr, int port, int len, Message msg, String str)
   {  if (log_all_packets || len>=MIN_MESSAGE_LENGTH)
      {  if (message_log!=null)
         {  message_log.printPacketTimestamp(proto,addr,port,len,str+"\r\n"+msg.toString()+"-----End-of-message-----\r\n",1);
         }
         if (event_log!=null)
         {  String first_line=msg.getFirstLine();
            if (first_line!=null) first_line=first_line.trim(); else first_line="NOT a SIP message";
            event_log.print("\r\n");
            event_log.printPacketTimestamp(proto,addr,port,len,first_line+", "+str,1);
            event_log.print("\r\n");
         }
      }
   }

}
